package nl.tudelft.lifetiles.sequence.controller; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.ResourceBundle; import java.util.Set; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableMap; import javafx.fxml.FXML; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.CheckBoxTableCell; import nl.tudelft.lifetiles.core.controller.AbstractController; import nl.tudelft.lifetiles.core.util.Logging; import nl.tudelft.lifetiles.core.util.Message; import nl.tudelft.lifetiles.notification.controller.NotificationController; import nl.tudelft.lifetiles.notification.model.NotificationFactory; import nl.tudelft.lifetiles.sequence.model.Sequence; import nl.tudelft.lifetiles.sequence.model.SequenceEntry; import nl.tudelft.lifetiles.sequence.model.SequenceMetaParser; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * The controller of the data view. * * @author Joren Hammudoglu * */ public class SequenceController extends AbstractController { /** * Shout message indicating a sequence has been set as reference. */ public static final Message REFERENCE_SET = Message.create("referenceSet"); /** * The default reference sequence name. */ private static final String DEFAULT_REFERENCE = "TKK_REF"; /** * The sequence table. */ @FXML private TableView<SequenceEntry> sequenceTable; /** * The table column indiciating sequence visibility. */ @FXML private TableColumn<SequenceEntry, Boolean> visibleColumn; /** * The table column indiciating if a sequence is the reference. */ @FXML private TableColumn<SequenceEntry, Boolean> referenceColumn; /** * The model of sequences. */ private Map<String, Sequence> sequences; /** * Set containing the currently visible sequences. */ private Set<Sequence> visibleSequences; /** * The sequence entries for in the table. */ private ObservableMap<String, SequenceEntry> sequenceEntries; /** * The index of the reference sequence entry. */ private String reference; /** * The listeners for the visible properties. */ private Map<SequenceEntry, ChangeListener<? super Boolean>> visibleListeners; /** * {@inheritDoc} */ @Override public void initialize(final URL location, final ResourceBundle resources) { registerShoutListeners(); initializeTable(); visibleListeners = new HashMap<>(); } /** * Register the shout listeners. */ // checkstyle bug causes false positives in our assert, so suppress. @SuppressWarnings("checkstyle:genericwhitespace") private void registerShoutListeners() { listen(Message.LOADED, (sender, subject, args) -> { if (!"sequences".equals(subject)) { return; } // eclipse and PMD disagree on whether parentheses are // needed assert args[0] instanceof Map<?, ?>; @SuppressWarnings("unchecked") Map<String, Sequence> sequences = (Map<String, Sequence>) args[0]; load(sequences); }); listen(Message.FILTERED, (sender, subject, args) -> { assert args.length == 1; assert args[0] instanceof Set<?>; @SuppressWarnings("unchecked") Set<Sequence> sequences = (Set<Sequence>) args[0]; updateVisible(sequences); }); listen(Message.RESET, (sender, subject, args) -> { Set<Sequence> visibleSet = new HashSet<>(sequences.values()); shout(Message.FILTERED, "", visibleSet); }); listen(Message.OPENED, (sender, subject, args) -> { if (!"meta".equals(subject)) { return; } assert args[0] instanceof File; SequenceMetaParser parser = new SequenceMetaParser(); try { parser.parse((File) args[0]); addMetaData(parser.getColumns(), parser.getData()); } catch (IOException exception) { Logging.exception(exception); shout(NotificationController.NOTIFY, "", new NotificationFactory() .getNotification(exception)); } }); } /** * Add the meta data to the appropriate sequence entries. * * @param columns * The column names * @param data * The actual data */ private void addMetaData(final List<String> columns, final Map<String, Map<String, String>> data) { for (Entry<String, Map<String, String>> sequenceMeta : data.entrySet()) { String identifier = sequenceMeta.getKey(); if (sequenceEntries.containsKey(identifier)) { SequenceEntry sequenceEntry = sequenceEntries.get(identifier); sequenceEntry.setMetaData(sequenceMeta.getValue()); } } addMetaColumns(columns); } /** * Load in the new sequences. * * @param sequences * the new sequences */ private void load(final Map<String, Sequence> sequences) { this.sequences = sequences; this.visibleSequences = new HashSet<>(sequences.values()); initializeEntries(sequences); populateTable(); } /** * Initialize and populate the table. * TODO display colors */ private void populateTable() { sequenceTable.setItems(FXCollections .observableArrayList(sequenceEntries.values())); } /** * Initialize the table. */ private void initializeTable() { sequenceTable.setEditable(true); visibleColumn.setCellFactory(CheckBoxTableCell .forTableColumn(visibleColumn)); visibleColumn.setEditable(true); referenceColumn.setCellFactory(CheckBoxTableCell .forTableColumn(referenceColumn)); referenceColumn.setEditable(true); } /** * Add the meta data columns to the table. * * @param columns * the names of the columns */ private void addMetaColumns(final List<String> columns) { for (String columnName : columns) { // purpose of this method is to create these. @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") TableColumn<SequenceEntry, String> column = new TableColumn<>( columnName); column.setCellValueFactory(entry -> entry.getValue().metaProperty( columnName)); sequenceTable.getColumns().add(column); } } /** * Generate Sequence entries from sequences and store them. * * @param sequences * the sequences */ // suppress a false positive on SequenceEntry @SuppressFBWarnings("DLS_DEAD_LOCAL_STORE") private void initializeEntries(final Map<String, Sequence> sequences) { sequenceEntries = FXCollections.observableHashMap(); for (Sequence sequence : sequences.values()) { SequenceEntry sequenceEntry = SequenceEntry.fromSequence(sequence); String identifier = sequence.getIdentifier(); if (identifier.equals(DEFAULT_REFERENCE)) { sequenceEntry = SequenceEntry .fromSequence(sequence, true, true); reference = identifier; shout(SequenceController.REFERENCE_SET, "", sequence); } else { sequenceEntry = SequenceEntry.fromSequence(sequence); } addVisibilityListener(sequenceEntry); addReferenceListener(sequenceEntry); sequenceEntries.put(identifier, sequenceEntry); } } /** * Add listener to the visible and reference properties of a sequence entry. * * @param entry * the sequence entry */ private void addVisibilityListener(final SequenceEntry entry) { final ChangeListener<? super Boolean> listener; if (visibleListeners.containsKey(entry)) { listener = visibleListeners.get(entry); } else { listener = (value, previous, current) -> { if (previous != current) { updateVisible(entry, current); shout(Message.FILTERED, "", visibleSequences); } }; visibleListeners.put(entry, listener); } entry.visibleProperty().addListener(listener); } /** * Remove the visibility listener from the entry. * * @param entry * the entry */ private void removeVisibilityListener(final SequenceEntry entry) { if (!visibleListeners.containsKey(entry)) { throw new IllegalArgumentException("Entry " + entry.getIdentifier() + " has no listener"); } entry.visibleProperty().removeListener(visibleListeners.get(entry)); } /** * Add listener to the sequence entry's reference property. * * @param entry * the sequence entry. */ private void addReferenceListener(final SequenceEntry entry) { entry.referenceProperty().addListener( (value, previous, current) -> { if (previous != current && !previous) { SequenceEntry previousRef = sequenceEntries .get(reference); previousRef.setReference(false); String identifier = entry.getIdentifier(); reference = identifier; shout(SequenceController.REFERENCE_SET, "", sequences.get(identifier)); } }); } /** * Update a sequence's visiblity and shout new visible sequences. * * @param entry * the sequence entry * @param visible * whether the sequence became visible */ private void updateVisible(final SequenceEntry entry, final boolean visible) { Sequence sequence = this.sequences.get(entry.getIdentifier()); if (visible) { visibleSequences.add(sequence); } else { visibleSequences.remove(sequence); } } /** * Update to the new visibles. * * @param visibles * the new visibles */ private void updateVisible(final Set<Sequence> visibles) { if (!sequences.values().containsAll(visibles)) { throw new IllegalArgumentException("Unknown sequences"); } for (Sequence sequence : sequences.values()) { boolean visible = visibles.contains(sequence); SequenceEntry entry = sequenceEntries.get(sequence.getIdentifier()); // temporarily stop listening to prevent circular shouting removeVisibilityListener(entry); entry.visibleProperty().setValue(visible); addVisibilityListener(entry); } visibleSequences = visibles; } }